<?php
/**
 * phpQuery is a server-side, chainable, CSS3 selector driven
 * Document Object Model (DOM) API based on jQuery JavaScript Library.
 *
 * @version 0.9.5
 * @link http://code.google.com/p/phpquery/
 * @link http://phpquery-library.blogspot.com/
 * @link http://jquery.com/
 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 * @package phpQuery
 */

// class names for instanceof
// TODO move them as class constants into phpQuery
define('DOMDOCUMENT', 'DOMDocument');
define('DOMELEMENT', 'DOMElement');
define('DOMNODELIST', 'DOMNodeList');
define('DOMNODE', 'DOMNode');
require_once(dirname(__FILE__) . '/phpQuery/DOMEvent.php');
require_once(dirname(__FILE__) . '/phpQuery/DOMDocumentWrapper.php');
require_once(dirname(__FILE__) . '/phpQuery/phpQueryEvents.php');
require_once(dirname(__FILE__) . '/phpQuery/Callback.php');
require_once(dirname(__FILE__) . '/phpQuery/phpQueryObject.php');
require_once(dirname(__FILE__) . '/phpQuery/compat/mbstring.php');

/**
 * Static namespace for phpQuery functions.
 *
 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 * @package phpQuery
 */
abstract class phpQuery
{
    /**
     * XXX: Workaround for mbstring problems
     *
     * @var bool
     */
    public static $mbstringSupport = true;
    public static $debug = false;
    public static $documents = array();
    public static $defaultDocumentID = null;
//	public static $defaultDoctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"';
    /**
     * Applies only to HTML.
     *
     * @var unknown_type
     */
    public static $defaultDoctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">';
    public static $defaultCharset = 'UTF-8';
    /**
     * Static namespace for plugins.
     *
     * @var object
     */
    public static $plugins = array();
    /**
     * List of loaded plugins.
     *
     * @var unknown_type
     */
    public static $pluginsLoaded = array();
    public static $pluginsMethods = array();
    public static $pluginsStaticMethods = array();
    public static $extendMethods = array();
    /**
     * @TODO implement
     */
    public static $extendStaticMethods = array();
    /**
     * Hosts allowed for AJAX connections.
     * Dot '.' means $_SERVER['HTTP_HOST'] (if any).
     *
     * @var array
     */
    public static $ajaxAllowedHosts = array(
        '.'
    );
    /**
     * AJAX settings.
     *
     * @var array
     * XXX should it be static or not ?
     */
    public static $ajaxSettings = array(
        'url' => '',//TODO
        'global' => true,
        'type' => "GET",
        'timeout' => null,
        'contentType' => "application/x-www-form-urlencoded",
        'processData' => true,
//		'async' => true,
        'data' => null,
        'username' => null,
        'password' => null,
        'dataType' => null,
        'ifModified' => null,
        'accepts' => array(
            'xml' => "application/xml, text/xml",
            'html' => "text/html",
            'script' => "text/javascript, application/javascript",
            'json' => "application/json, text/javascript",
            'text' => "text/plain",
            '_default' => "*/*"
        )
    );
    public static $lastModified = null;
    public static $active = 0;
    public static $dumpCount = 0;

    /**
     * Multi-purpose function.
     * Use pq() as shortcut.
     *
     * In below examples, $pq is any result of pq(); function.
     *
     * 1. Import markup into existing document (without any attaching):
     * - Import into selected document:
     *   pq('<div/>')                // DOESNT accept text nodes at beginning of input string !
     * - Import into document with ID from $pq->getDocumentID():
     *   pq('<div/>', $pq->getDocumentID())
     * - Import into same document as DOMNode belongs to:
     *   pq('<div/>', DOMNode)
     * - Import into document from phpQuery object:
     *   pq('<div/>', $pq)
     *
     * 2. Run query:
     * - Run query on last selected document:
     *   pq('div.myClass')
     * - Run query on document with ID from $pq->getDocumentID():
     *   pq('div.myClass', $pq->getDocumentID())
     * - Run query on same document as DOMNode belongs to and use node(s)as root for query:
     *   pq('div.myClass', DOMNode)
     * - Run query on document from phpQuery object
     *   and use object's stack as root node(s) for query:
     *   pq('div.myClass', $pq)
     *
     * @param string|DOMNode|DOMNodeList|array $arg1 HTML markup, CSS Selector, DOMNode or array of DOMNodes
     * @param string|phpQueryObject|DOMNode $context DOM ID from $pq->getDocumentID(), phpQuery object (determines also query root) or DOMNode (determines also query root)
     *
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery|QueryTemplatesPhpQuery|false
     * phpQuery object or false in case of error.
     */
    public static function pq($arg1, $context = null)
    {
        if ($arg1 instanceof DOMNODE && !isset($context)) {
            foreach (phpQuery::$documents as $documentWrapper) {
                $compare = $arg1 instanceof DOMDocument
                    ? $arg1 : $arg1->ownerDocument;
                if ($documentWrapper->document->isSameNode($compare))
                    $context = $documentWrapper->id;
            }
        }
        if (!$context) {
            $domId = self::$defaultDocumentID;
            if (!$domId)
                throw new Exception("Can't use last created DOM, because there isn't any. Use phpQuery::newDocument() first.");
//		} else if (is_object($context) && ($context instanceof PHPQUERY || is_subclass_of($context, 'phpQueryObject')))
        } else if (is_object($context) && $context instanceof phpQueryObject)
            $domId = $context->getDocumentID();
        else if ($context instanceof DOMDOCUMENT) {
            $domId = self::getDocumentID($context);
            if (!$domId) {
                //throw new Exception('Orphaned DOMDocument');
                $domId = self::newDocument($context)->getDocumentID();
            }
        } else if ($context instanceof DOMNODE) {
            $domId = self::getDocumentID($context);
            if (!$domId) {
                throw new Exception('Orphaned DOMNode');
//				$domId = self::newDocument($context->ownerDocument);
            }
        } else
            $domId = $context;
        if ($arg1 instanceof phpQueryObject) {
//		if (is_object($arg1) && (get_class($arg1) == 'phpQueryObject' || $arg1 instanceof PHPQUERY || is_subclass_of($arg1, 'phpQueryObject'))) {
            /**
             * Return $arg1 or import $arg1 stack if document differs:
             * pq(pq('<div/>'))
             */
            if ($arg1->getDocumentID() == $domId)
                return $arg1;
            $class = get_class($arg1);
            // support inheritance by passing old object to overloaded constructor
            $phpQuery = $class != 'phpQuery'
                ? new $class($arg1, $domId)
                : new phpQueryObject($domId);
            $phpQuery->elements = array();
            foreach ($arg1->elements as $node)
                $phpQuery->elements[] = $phpQuery->document->importNode($node, true);
            return $phpQuery;
        } else if ($arg1 instanceof DOMNODE || (is_array($arg1) && isset($arg1[0]) && $arg1[0] instanceof DOMNODE)) {
            /*
             * Wrap DOM nodes with phpQuery object, import into document when needed:
             * pq(array($domNode1, $domNode2))
             */
            $phpQuery = new phpQueryObject($domId);
            if (!($arg1 instanceof DOMNODELIST) && !is_array($arg1))
                $arg1 = array($arg1);
            $phpQuery->elements = array();
            foreach ($arg1 as $node) {
                $sameDocument = $node->ownerDocument instanceof DOMDOCUMENT
                    && !$node->ownerDocument->isSameNode($phpQuery->document);
                $phpQuery->elements[] = $sameDocument
                    ? $phpQuery->document->importNode($node, true)
                    : $node;
            }
            return $phpQuery;
        } else if (self::isMarkup($arg1)) {
            /**
             * Import HTML:
             * pq('<div/>')
             */
            $phpQuery = new phpQueryObject($domId);
            return $phpQuery->newInstance(
                $phpQuery->documentWrapper->import($arg1)
            );
        } else {
            /**
             * Run CSS query:
             * pq('div.myClass')
             */
            $phpQuery = new phpQueryObject($domId);
//			if ($context && ($context instanceof PHPQUERY || is_subclass_of($context, 'phpQueryObject')))
            if ($context && $context instanceof phpQueryObject)
                $phpQuery->elements = $context->elements;
            else if ($context && $context instanceof DOMNODELIST) {
                $phpQuery->elements = array();
                foreach ($context as $node)
                    $phpQuery->elements[] = $node;
            } else if ($context && $context instanceof DOMNODE)
                $phpQuery->elements = array($context);
            return $phpQuery->find($arg1);
        }
    }

    /**
     * Sets default document to $id. Document has to be loaded prior
     * to using this method.
     * $id can be retrived via getDocumentID() or getDocumentIDRef().
     *
     * @param unknown_type $id
     */
    public static function selectDocument($id)
    {
        $id = self::getDocumentID($id);
        self::debug("Selecting document '$id' as default one");
        self::$defaultDocumentID = self::getDocumentID($id);
    }

    /**
     * Returns document with id $id or last used as phpQueryObject.
     * $id can be retrived via getDocumentID() or getDocumentIDRef().
     * Chainable.
     *
     * @param unknown_type $id
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     * @see phpQuery::selectDocument()
     */
    public static function getDocument($id = null)
    {
        if ($id)
            phpQuery::selectDocument($id);
        else
            $id = phpQuery::$defaultDocumentID;
        return new phpQueryObject($id);
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocument($markup = null, $contentType = null)
    {
        if (!$markup)
            $markup = '';
        $documentID = phpQuery::createDocumentWrapper($markup, $contentType);
        return new phpQueryObject($documentID);
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentHTML($markup = null, $charset = null)
    {
        $contentType = $charset
            ? ";charset=$charset"
            : '';
        return self::newDocument($markup, "text/html{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentXML($markup = null, $charset = null)
    {
        $contentType = $charset
            ? ";charset=$charset"
            : '';
        return self::newDocument($markup, "text/xml{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentXHTML($markup = null, $charset = null)
    {
        $contentType = $charset
            ? ";charset=$charset"
            : '';
        return self::newDocument($markup, "application/xhtml+xml{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentPHP($markup = null, $contentType = "text/html")
    {
        // TODO pass charset to phpToMarkup if possible (use DOMDocumentWrapper function)
        $markup = phpQuery::phpToMarkup($markup, self::$defaultCharset);
        return self::newDocument($markup, $contentType);
    }

    public static function phpToMarkup($php, $charset = 'utf-8')
    {
        $regexes = array(
            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(\')([^\']*)<' . '?php?(.*?)(?:\\?>)([^\']*)\'@s',
            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(")([^"]*)<' . '?php?(.*?)(?:\\?>)([^"]*)"@s',
        );
        foreach ($regexes as $regex)
            while (preg_match($regex, $php, $matches)) {
                $php = preg_replace_callback(
                    $regex,
//					create_function('$m, $charset = "'.$charset.'"',
//						'return $m[1].$m[2]
//							.htmlspecialchars("<"."?php".$m[4]."?".">", ENT_QUOTES|ENT_NOQUOTES, $charset)
//							.$m[5].$m[2];'
//					),
                    array('phpQuery', '_phpToMarkupCallback'),
                    $php
                );
            }
        $regex = '@(^|>[^<]*)+?(<\?php(.*?)(\?>))@s';
//preg_match_all($regex, $php, $matches);
//var_dump($matches);
        $php = preg_replace($regex, '\\1<php><!-- \\3 --></php>', $php);
        return $php;
    }

    public static function _phpToMarkupCallback($php, $charset = 'utf-8')
    {
        return $m[1] . $m[2]
            . htmlspecialchars("<" . "?php" . $m[4] . "?" . ">", ENT_QUOTES | ENT_NOQUOTES, $charset)
            . $m[5] . $m[2];
    }

    public static function _markupToPHPCallback($m)
    {
        return "<" . "?php " . htmlspecialchars_decode($m[1]) . " ?" . ">";
    }

    /**
     * Converts document markup containing PHP code generated by phpQuery::php()
     * into valid (executable) PHP code syntax.
     *
     * @param string|phpQueryObject $content
     * @return string PHP code.
     */
    public static function markupToPHP($content)
    {
        if ($content instanceof phpQueryObject)
            $content = $content->markupOuter();
        /* <php>...</php> to <?php...? > */
        $content = preg_replace_callback(
            '@<php>\s*<!--(.*?)-->\s*</php>@s',
//			create_function('$m',
//				'return "<'.'?php ".htmlspecialchars_decode($m[1])." ?'.'>";'
//			),
            array('phpQuery', '_markupToPHPCallback'),
            $content
        );
        /* <node attr='< ?php ? >'> extra space added to save highlighters */
        $regexes = array(
            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(\')([^\']*)(?:&lt;|%3C)\\?(?:php)?(.*?)(?:\\?(?:&gt;|%3E))([^\']*)\'@s',
            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(")([^"]*)(?:&lt;|%3C)\\?(?:php)?(.*?)(?:\\?(?:&gt;|%3E))([^"]*)"@s',
        );
        foreach ($regexes as $regex)
            while (preg_match($regex, $content))
                $content = preg_replace_callback(
                    $regex,
                    create_function('$m',
                        'return $m[1].$m[2].$m[3]."<?php "
							.str_replace(
								array("%20", "%3E", "%09", "&#10;", "&#9;", "%7B", "%24", "%7D", "%22", "%5B", "%5D"),
								array(" ", ">", "	", "\n", "	", "{", "$", "}", \'"\', "[", "]"),
								htmlspecialchars_decode($m[4])
							)
							." ?>".$m[5].$m[2];'
                    ),
                    $content
                );
        return $content;
    }

    /**
     * Creates new document from file $file.
     * Chainable.
     *
     * @param string $file URLs allowed. See File wrapper page at php.net for more supported sources.
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFile($file, $contentType = null)
    {
        $documentID = self::createDocumentWrapper(
            file_get_contents($file), $contentType
        );
        return new phpQueryObject($documentID);
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFileHTML($file, $charset = null)
    {
        $contentType = $charset
            ? ";charset=$charset"
            : '';
        return self::newDocumentFile($file, "text/html{$contentType}");
    }


    public static function newDocumentURL($url, $contentType = null)
    {
        $content = self::curl_get($url);
        $documentID = self::createDocumentWrapper(
            $content, $contentType
        );
        return new phpQueryObject($documentID);
    }

    public static function curl_get($url)
    {

        $header = [
            'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
            'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36'
        ];

        $curl = curl_init();
        //设置抓取的url
        curl_setopt($curl, CURLOPT_URL, $url);
        //设置头文件的信息作为数据流输出
        curl_setopt($curl, CURLOPT_HEADER, 0);
        // 超时设置,以秒为单位
        curl_setopt($curl, CURLOPT_TIMEOUT, 10);

        // 超时设置，以毫秒为单位
        // curl_setopt($curl, CURLOPT_TIMEOUT_MS, 500);

        // 设置请求头
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
        //设置获取的信息以文件流的形式返回，而不是直接输出。
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
        //执行命令
        $data = curl_exec($curl);

        // 显示错误信息
        if (curl_error($curl)) {
            print "Error: " . curl_error($curl);
        } else {
            curl_close($curl);
            return $data;
        }
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFileXML($file, $charset = null)
    {
        $contentType = $charset
            ? ";charset=$charset"
            : '';
        return self::newDocumentFile($file, "text/xml{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFileXHTML($file, $charset = null)
    {
        $contentType = $charset
            ? ";charset=$charset"
            : '';
        return self::newDocumentFile($file, "application/xhtml+xml{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFilePHP($file, $contentType = null)
    {
        return self::newDocumentPHP(file_get_contents($file), $contentType);
    }

    /**
     * Reuses existing DOMDocument object.
     * Chainable.
     *
     * @param $document DOMDocument
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     * @TODO support DOMDocument
     */
    public static function loadDocument($document)
    {
        // TODO
        die('TODO loadDocument');
    }

    /**
     * Enter description here...
     *
     * @param unknown_type $html
     * @param unknown_type $domId
     * @return unknown New DOM ID
     * @todo support PHP tags in input
     * @todo support passing DOMDocument object from self::loadDocument
     */
    protected static function createDocumentWrapper($html, $contentType = null, $documentID = null)
    {
        if (function_exists('domxml_open_mem'))
            throw new Exception("Old PHP4 DOM XML extension detected. phpQuery won't work until this extension is enabled.");
//		$id = $documentID
//			? $documentID
//			: md5(microtime());
        $document = null;
        if ($html instanceof DOMDOCUMENT) {
            if (self::getDocumentID($html)) {
                // document already exists in phpQuery::$documents, make a copy
                $document = clone $html;
            } else {
                // new document, add it to phpQuery::$documents
                $wrapper = new DOMDocumentWrapper($html, $contentType, $documentID);
            }
        } else {
            $wrapper = new DOMDocumentWrapper($html, $contentType, $documentID);
        }
//		$wrapper->id = $id;
        // bind document
        phpQuery::$documents[$wrapper->id] = $wrapper;
        // remember last loaded document
        phpQuery::selectDocument($wrapper->id);
        return $wrapper->id;
    }

    /**
     * Extend class namespace.
     *
     * @param string|array $target
     * @param array $source
     * @TODO support string $source
     * @return unknown_type
     */
    public static function extend($target, $source)
    {
        switch ($target) {
            case 'phpQueryObject':
                $targetRef = &self::$extendMethods;
                $targetRef2 = &self::$pluginsMethods;
                break;
            case 'phpQuery':
                $targetRef = &self::$extendStaticMethods;
                $targetRef2 = &self::$pluginsStaticMethods;
                break;
            default:
                throw new Exception("Unsupported \$target type");
        }
        if (is_string($source))
            $source = array($source => $source);
        foreach ($source as $method => $callback) {
            if (isset($targetRef[$method])) {
//				throw new Exception
                self::debug("Duplicate method '{$method}', can\'t extend '{$target}'");
                continue;
            }
            if (isset($targetRef2[$method])) {
//				throw new Exception
                self::debug("Duplicate method '{$method}' from plugin '{$targetRef2[$method]}',"
                    . " can\'t extend '{$target}'");
                continue;
            }
            $targetRef[$method] = $callback;
        }
        return true;
    }

    /**
     * Extend phpQuery with $class from $file.
     *
     * @param string $class Extending class name. Real class name can be prepended phpQuery_.
     * @param string $file Filename to include. Defaults to "{$class}.php".
     */
    public static function plugin($class, $file = null)
    {
        // TODO $class checked agains phpQuery_$class
//		if (strpos($class, 'phpQuery') === 0)
//			$class = substr($class, 8);
        if (in_array($class, self::$pluginsLoaded))
            return true;
        if (!$file)
            $file = $class . '.php';
        $objectClassExists = class_exists('phpQueryObjectPlugin_' . $class);
        $staticClassExists = class_exists('phpQueryPlugin_' . $class);
        if (!$objectClassExists && !$staticClassExists)
            require_once($file);
        self::$pluginsLoaded[] = $class;
        // static methods
        if (class_exists('phpQueryPlugin_' . $class)) {
            $realClass = 'phpQueryPlugin_' . $class;
            $vars = get_class_vars($realClass);
            $loop = isset($vars['phpQueryMethods'])
            && !is_null($vars['phpQueryMethods'])
                ? $vars['phpQueryMethods']
                : get_class_methods($realClass);
            foreach ($loop as $method) {
                if ($method == '__initialize')
                    continue;
                if (!is_callable(array($realClass, $method)))
                    continue;
                if (isset(self::$pluginsStaticMethods[$method])) {
                    throw new Exception("Duplicate method '{$method}' from plugin '{$c}' conflicts with same method from plugin '" . self::$pluginsStaticMethods[$method] . "'");
                    return;
                }
                self::$pluginsStaticMethods[$method] = $class;
            }
            if (method_exists($realClass, '__initialize'))
                call_user_func_array(array($realClass, '__initialize'), array());
        }
        // object methods
        if (class_exists('phpQueryObjectPlugin_' . $class)) {
            $realClass = 'phpQueryObjectPlugin_' . $class;
            $vars = get_class_vars($realClass);
            $loop = isset($vars['phpQueryMethods'])
            && !is_null($vars['phpQueryMethods'])
                ? $vars['phpQueryMethods']
                : get_class_methods($realClass);
            foreach ($loop as $method) {
                if (!is_callable(array($realClass, $method)))
                    continue;
                if (isset(self::$pluginsMethods[$method])) {
                    throw new Exception("Duplicate method '{$method}' from plugin '{$c}' conflicts with same method from plugin '" . self::$pluginsMethods[$method] . "'");
                    continue;
                }
                self::$pluginsMethods[$method] = $class;
            }
        }
        return true;
    }

    /**
     * Unloades all or specified document from memory.
     *
     * @param mixed $documentID @see phpQuery::getDocumentID() for supported types.
     */
    public static function unloadDocuments($id = null)
    {
        if (isset($id)) {
            if ($id = self::getDocumentID($id))
                unset(phpQuery::$documents[$id]);
        } else {
            foreach (phpQuery::$documents as $k => $v) {
                unset(phpQuery::$documents[$k]);
            }
        }
    }

    /**
     * Parses phpQuery object or HTML result against PHP tags and makes them active.
     *
     * @param phpQuery|string $content
     * @return string
     * @deprecated
     */
    public static function unsafePHPTags($content)
    {
        return self::markupToPHP($content);
    }

    public static function DOMNodeListToArray($DOMNodeList)
    {
        $array = array();
        if (!$DOMNodeList)
            return $array;
        foreach ($DOMNodeList as $node)
            $array[] = $node;
        return $array;
    }

    /**
     * Checks if $input is HTML string, which has to start with '<'.
     *
     * @param String $input
     * @return Bool
     * @deprecated
     * @todo still used ?
     */
    public static function isMarkup($input)
    {
        return !is_array($input) && substr(trim($input), 0, 1) == '<';
    }

    public static function debug($text)
    {
        if (self::$debug)
            print var_dump($text);
    }

    /**
     * Make an AJAX request.
     *
     * @param array See $options http://docs.jquery.com/Ajax/jQuery.ajax#toptions
     * Additional options are:
     * 'document' - document for global events, @see phpQuery::getDocumentID()
     * 'referer' - implemented
     * 'requested_with' - TODO; not implemented (X-Requested-With)
     * @return Zend_Http_Client
     * @link http://docs.jquery.com/Ajax/jQuery.ajax
     *
     * @TODO $options['cache']
     * @TODO $options['processData']
     * @TODO $options['xhr']
     * @TODO $options['data'] as string
     * @TODO XHR interface
     */
    public static function ajax($options = array(), $xhr = null)
    {
        $options = array_merge(
            self::$ajaxSettings, $options
        );
        $documentID = isset($options['document'])
            ? self::getDocumentID($options['document'])
            : null;
        if ($xhr) {
            // reuse existing XHR object, but clean it up
            $client = $xhr;
//			$client->setParameterPost(null);
//			$client->setParameterGet(null);
            $client->setAuth(false);
            $client->setHeaders("If-Modified-Since", null);
            $client->setHeaders("Referer", null);
            $client->resetParameters();
        } else {
            // create new XHR object
            require_once('Zend/Http/Client.php');
            $client = new Zend_Http_Client();
            $client->setCookieJar();
        }
        if (isset($options['timeout']))
            $client->setConfig(array(
                'timeout' => $options['timeout'],
            ));
//			'maxredirects' => 0,
        foreach (self::$ajaxAllowedHosts as $k => $host)
            if ($host == '.' && isset($_SERVER['HTTP_HOST']))
                self::$ajaxAllowedHosts[$k] = $_SERVER['HTTP_HOST'];
        $host = parse_url($options['url'], PHP_URL_HOST);
        if (!in_array($host, self::$ajaxAllowedHosts)) {
            throw new Exception("Request not permitted, host '$host' not present in "
                . "phpQuery::\$ajaxAllowedHosts");
        }
        // JSONP
        $jsre = "/=\\?(&|$)/";
        if (isset($options['dataType']) && $options['dataType'] == 'jsonp') {
            $jsonpCallbackParam = $options['jsonp']
                ? $options['jsonp'] : 'callback';
            if (strtolower($options['type']) == 'get') {
                if (!preg_match($jsre, $options['url'])) {
                    $sep = strpos($options['url'], '?')
                        ? '&' : '?';
                    $options['url'] .= "$sep$jsonpCallbackParam=?";
                }
            } else if ($options['data']) {
                $jsonp = false;
                foreach ($options['data'] as $n => $v) {
                    if ($v == '?')
                        $jsonp = true;
                }
                if (!$jsonp) {
                    $options['data'][$jsonpCallbackParam] = '?';
                }
            }
            $options['dataType'] = 'json';
        }
        if (isset($options['dataType']) && $options['dataType'] == 'json') {
            $jsonpCallback = 'json_' . md5(microtime());
            $jsonpData = $jsonpUrl = false;
            if ($options['data']) {
                foreach ($options['data'] as $n => $v) {
                    if ($v == '?')
                        $jsonpData = $n;
                }
            }
            if (preg_match($jsre, $options['url']))
                $jsonpUrl = true;
            if ($jsonpData !== false || $jsonpUrl) {
                // remember callback name for httpData()
                $options['_jsonp'] = $jsonpCallback;
                if ($jsonpData !== false)
                    $options['data'][$jsonpData] = $jsonpCallback;
                if ($jsonpUrl)
                    $options['url'] = preg_replace($jsre, "=$jsonpCallback\\1", $options['url']);
            }
        }
        $client->setUri($options['url']);
        $client->setMethod(strtoupper($options['type']));
        if (isset($options['referer']) && $options['referer'])
            $client->setHeaders('Referer', $options['referer']);
        $client->setHeaders(array(
//			'content-type' => $options['contentType'],
            'User-Agent' => 'Mozilla/5.0 (X11; U; Linux x86; en-US; rv:1.9.0.5) Gecko'
                . '/2008122010 Firefox/3.0.5',
            // TODO custom charset
            'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
// 	 		'Connection' => 'keep-alive',
// 			'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language' => 'en-us,en;q=0.5',
        ));
        if ($options['username'])
            $client->setAuth($options['username'], $options['password']);
        if (isset($options['ifModified']) && $options['ifModified'])
            $client->setHeaders("If-Modified-Since",
                self::$lastModified
                    ? self::$lastModified
                    : "Thu, 01 Jan 1970 00:00:00 GMT"
            );
        $client->setHeaders("Accept",
            isset($options['dataType'])
            && isset(self::$ajaxSettings['accepts'][$options['dataType']])
                ? self::$ajaxSettings['accepts'][$options['dataType']] . ", */*"
                : self::$ajaxSettings['accepts']['_default']
        );
        // TODO $options['processData']
        if ($options['data'] instanceof phpQueryObject) {
            $serialized = $options['data']->serializeArray($options['data']);
            $options['data'] = array();
            foreach ($serialized as $r)
                $options['data'][$r['name']] = $r['value'];
        }
        if (strtolower($options['type']) == 'get') {
            $client->setParameterGet($options['data']);
        } else if (strtolower($options['type']) == 'post') {
            $client->setEncType($options['contentType']);
            $client->setParameterPost($options['data']);
        }
        if (self::$active == 0 && $options['global'])
            phpQueryEvents::trigger($documentID, 'ajaxStart');
        self::$active++;
        // beforeSend callback
        if (isset($options['beforeSend']) && $options['beforeSend'])
            phpQuery::callbackRun($options['beforeSend'], array($client));
        // ajaxSend event
        if ($options['global'])
            phpQueryEvents::trigger($documentID, 'ajaxSend', array($client, $options));
        if (phpQuery::$debug) {
            self::debug("{$options['type']}: {$options['url']}\n");
            self::debug("Options: <pre>" . var_export($options, true) . "</pre>\n");
//			if ($client->getCookieJar())
//				self::debug("Cookies: <pre>".var_export($client->getCookieJar()->getMatchingCookies($options['url']), true)."</pre>\n");
        }
        // request
        $response = $client->request();
        if (phpQuery::$debug) {
            self::debug('Status: ' . $response->getStatus() . ' / ' . $response->getMessage());
            self::debug($client->getLastRequest());
            self::debug($response->getHeaders());
        }
        if ($response->isSuccessful()) {
            // XXX tempolary
            self::$lastModified = $response->getHeader('Last-Modified');
            $data = self::httpData($response->getBody(), $options['dataType'], $options);
            if (isset($options['success']) && $options['success'])
                phpQuery::callbackRun($options['success'], array($data, $response->getStatus(), $options));
            if ($options['global'])
                phpQueryEvents::trigger($documentID, 'ajaxSuccess', array($client, $options));
        } else {
            if (isset($options['error']) && $options['error'])
                phpQuery::callbackRun($options['error'], array($client, $response->getStatus(), $response->getMessage()));
            if ($options['global'])
                phpQueryEvents::trigger($documentID, 'ajaxError', array($client, /*$response->getStatus(),*/ $response->getMessage(), $options));
        }
        if (isset($options['complete']) && $options['complete'])
            phpQuery::callbackRun($options['complete'], array($client, $response->getStatus()));
        if ($options['global'])
            phpQueryEvents::trigger($documentID, 'ajaxComplete', array($client, $options));
        if ($options['global'] && !--self::$active)
            phpQueryEvents::trigger($documentID, 'ajaxStop');
        return $client;
//		if (is_null($domId))
//			$domId = self::$defaultDocumentID ? self::$defaultDocumentID : false;
//		return new phpQueryAjaxResponse($response, $domId);
    }

    protected static function httpData($data, $type, $options)
    {
        if (isset($options['dataFilter']) && $options['dataFilter'])
            $data = self::callbackRun($options['dataFilter'], array($data, $type));
        if (is_string($data)) {
            if ($type == "json") {
                if (isset($options['_jsonp']) && $options['_jsonp']) {
                    $data = preg_replace('/^\s*\w+\((.*)\)\s*$/s', '$1', $data);
                }
                $data = self::parseJSON($data);
            }
        }
        return $data;
    }

    /**
     * Enter description here...
     *
     * @param array|phpQuery $data
     *
     */
    public static function param($data)
    {
        return http_build_query($data, null, '&');
    }

    public static function get($url, $data = null, $callback = null, $type = null)
    {
        if (!is_array($data)) {
            $callback = $data;
            $data = null;
        }
        // TODO some array_values on this shit
        return phpQuery::ajax(array(
            'type' => 'GET',
            'url' => $url,
            'data' => $data,
            'success' => $callback,
            'dataType' => $type,
        ));
    }

    public static function post($url, $data = null, $callback = null, $type = null)
    {
        if (!is_array($data)) {
            $callback = $data;
            $data = null;
        }
        return phpQuery::ajax(array(
            'type' => 'POST',
            'url' => $url,
            'data' => $data,
            'success' => $callback,
            'dataType' => $type,
        ));
    }

    public static function getJSON($url, $data = null, $callback = null)
    {
        if (!is_array($data)) {
            $callback = $data;
            $data = null;
        }
        // TODO some array_values on this shit
        return phpQuery::ajax(array(
            'type' => 'GET',
            'url' => $url,
            'data' => $data,
            'success' => $callback,
            'dataType' => 'json',
        ));
    }

    public static function ajaxSetup($options)
    {
        self::$ajaxSettings = array_merge(
            self::$ajaxSettings,
            $options
        );
    }

    public static function ajaxAllowHost($host1, $host2 = null, $host3 = null)
    {
        $loop = is_array($host1)
            ? $host1
            : func_get_args();
        foreach ($loop as $host) {
            if ($host && !in_array($host, phpQuery::$ajaxAllowedHosts)) {
                phpQuery::$ajaxAllowedHosts[] = $host;
            }
        }
    }

    public static function ajaxAllowURL($url1, $url2 = null, $url3 = null)
    {
        $loop = is_array($url1)
            ? $url1
            : func_get_args();
        foreach ($loop as $url)
            phpQuery::ajaxAllowHost(parse_url($url, PHP_URL_HOST));
    }

    /**
     * Returns JSON representation of $data.
     *
     * @static
     * @param mixed $data
     * @return string
     */
    public static function toJSON($data)
    {
        if (function_exists('json_encode'))
            return json_encode($data);
        require_once('Zend/Json/Encoder.php');
        return Zend_Json_Encoder::encode($data);
    }

    /**
     * Parses JSON into proper PHP type.
     *
     * @static
     * @param string $json
     * @return mixed
     */
    public static function parseJSON($json)
    {
        if (function_exists('json_decode')) {
            $return = json_decode(trim($json), true);
            // json_decode and UTF8 issues
            if (isset($return))
                return $return;
        }
        require_once('Zend/Json/Decoder.php');
        return Zend_Json_Decoder::decode($json);
    }

    /**
     * Returns source's document ID.
     *
     * @param $source DOMNode|phpQueryObject
     * @return string
     */
    public static function getDocumentID($source)
    {
        if ($source instanceof DOMDOCUMENT) {
            foreach (phpQuery::$documents as $id => $document) {
                if ($source->isSameNode($document->document))
                    return $id;
            }
        } else if ($source instanceof DOMNODE) {
            foreach (phpQuery::$documents as $id => $document) {
                if ($source->ownerDocument->isSameNode($document->document))
                    return $id;
            }
        } else if ($source instanceof phpQueryObject)
            return $source->getDocumentID();
        else if (is_string($source) && isset(phpQuery::$documents[$source]))
            return $source;
    }

    /**
     * Get DOMDocument object related to $source.
     * Returns null if such document doesn't exist.
     *
     * @param $source DOMNode|phpQueryObject|string
     * @return string
     */
    public static function getDOMDocument($source)
    {
        if ($source instanceof DOMDOCUMENT)
            return $source;
        $source = self::getDocumentID($source);
        return $source
            ? self::$documents[$id]['document']
            : null;
    }

    // UTILITIES
    // http://docs.jquery.com/Utilities

    /**
     *
     * @return unknown_type
     * @link http://docs.jquery.com/Utilities/jQuery.makeArray
     */
    public static function makeArray($obj)
    {
        $array = array();
        if (is_object($object) && $object instanceof DOMNODELIST) {
            foreach ($object as $value)
                $array[] = $value;
        } else if (is_object($object) && !($object instanceof Iterator)) {
            foreach (get_object_vars($object) as $name => $value)
                $array[0][$name] = $value;
        } else {
            foreach ($object as $name => $value)
                $array[0][$name] = $value;
        }
        return $array;
    }

    public static function inArray($value, $array)
    {
        return in_array($value, $array);
    }

    /**
     *
     * @param $object
     * @param $callback
     * @return unknown_type
     * @link http://docs.jquery.com/Utilities/jQuery.each
     */
    public static function each($object, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        $paramStructure = null;
        if (func_num_args() > 2) {
            $paramStructure = func_get_args();
            $paramStructure = array_slice($paramStructure, 2);
        }
        if (is_object($object) && !($object instanceof Iterator)) {
            foreach (get_object_vars($object) as $name => $value)
                phpQuery::callbackRun($callback, array($name, $value), $paramStructure);
        } else {
            foreach ($object as $name => $value)
                phpQuery::callbackRun($callback, array($name, $value), $paramStructure);
        }
    }

    /**
     *
     * @link http://docs.jquery.com/Utilities/jQuery.map
     */
    public static function map($array, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        $result = array();
        $paramStructure = null;
        if (func_num_args() > 2) {
            $paramStructure = func_get_args();
            $paramStructure = array_slice($paramStructure, 2);
        }
        foreach ($array as $v) {
            $vv = phpQuery::callbackRun($callback, array($v), $paramStructure);
//			$callbackArgs = $args;
//			foreach($args as $i => $arg) {
//				$callbackArgs[$i] = $arg instanceof CallbackParam
//					? $v
//					: $arg;
//			}
//			$vv = call_user_func_array($callback, $callbackArgs);
            if (is_array($vv)) {
                foreach ($vv as $vvv)
                    $result[] = $vvv;
            } else if ($vv !== null) {
                $result[] = $vv;
            }
        }
        return $result;
    }

    /**
     *
     * @param $callback Callback
     * @param $params
     * @param $paramStructure
     * @return unknown_type
     */
    public static function callbackRun($callback, $params = array(), $paramStructure = null)
    {
        if (!$callback)
            return;
        if ($callback instanceof CallbackParameterToReference) {
            // TODO support ParamStructure to select which $param push to reference
            if (isset($params[0]))
                $callback->callback = $params[0];
            return true;
        }
        if ($callback instanceof Callback) {
            $paramStructure = $callback->params;
            $callback = $callback->callback;
        }
        if (!$paramStructure)
            return call_user_func_array($callback, $params);
        $p = 0;
        foreach ($paramStructure as $i => $v) {
            $paramStructure[$i] = $v instanceof CallbackParam
                ? $params[$p++]
                : $v;
        }
        return call_user_func_array($callback, $paramStructure);
    }

    /**
     * Merge 2 phpQuery objects.
     * @param array $one
     * @param array $two
     * @protected
     * @todo node lists, phpQueryObject
     */
    public static function merge($one, $two)
    {
        $elements = $one->elements;
        foreach ($two->elements as $node) {
            $exists = false;
            foreach ($elements as $node2) {
                if ($node2->isSameNode($node))
                    $exists = true;
            }
            if (!$exists)
                $elements[] = $node;
        }
        return $elements;
//		$one = $one->newInstance();
//		$one->elements = $elements;
//		return $one;
    }

    /**
     *
     * @param $array
     * @param $callback
     * @param $invert
     * @return unknown_type
     * @link http://docs.jquery.com/Utilities/jQuery.grep
     */
    public static function grep($array, $callback, $invert = false)
    {
        $result = array();
        foreach ($array as $k => $v) {
            $r = call_user_func_array($callback, array($v, $k));
            if ($r === !(bool)$invert)
                $result[] = $v;
        }
        return $result;
    }

    public static function unique($array)
    {
        return array_unique($array);
    }

    /**
     *
     * @param $function
     * @return unknown_type
     * @TODO there are problems with non-static methods, second parameter pass it
     *    but doesnt verify is method is really callable
     */
    public static function isFunction($function)
    {
        return is_callable($function);
    }

    public static function trim($str)
    {
        return trim($str);
    }
    /* PLUGINS NAMESPACE */
    /**
     *
     * @param $url
     * @param $callback
     * @param $param1
     * @param $param2
     * @param $param3
     * @return phpQueryObject
     */
    public static function browserGet($url, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        if (self::plugin('WebBrowser')) {
            $params = func_get_args();
            return self::callbackRun(array(self::$plugins, 'browserGet'), $params);
        } else {
            self::debug('WebBrowser plugin not available...');
        }
    }

    /**
     *
     * @param $url
     * @param $data
     * @param $callback
     * @param $param1
     * @param $param2
     * @param $param3
     * @return phpQueryObject
     */
    public static function browserPost($url, $data, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        if (self::plugin('WebBrowser')) {
            $params = func_get_args();
            return self::callbackRun(array(self::$plugins, 'browserPost'), $params);
        } else {
            self::debug('WebBrowser plugin not available...');
        }
    }

    /**
     *
     * @param $ajaxSettings
     * @param $callback
     * @param $param1
     * @param $param2
     * @param $param3
     * @return phpQueryObject
     */
    public static function browser($ajaxSettings, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        if (self::plugin('WebBrowser')) {
            $params = func_get_args();
            return self::callbackRun(array(self::$plugins, 'browser'), $params);
        } else {
            self::debug('WebBrowser plugin not available...');
        }
    }

    /**
     *
     * @param $code
     * @return string
     */
    public static function php($code)
    {
        return self::code('php', $code);
    }

    /**
     *
     * @param $type
     * @param $code
     * @return string
     */
    public static function code($type, $code)
    {
        return "<$type><!-- " . trim($code) . " --></$type>";
    }

    public static function __callStatic($method, $params)
    {
        return call_user_func_array(
            array(phpQuery::$plugins, $method),
            $params
        );
    }

    protected static function dataSetupNode($node, $documentID)
    {
        // search are return if alredy exists
        foreach (phpQuery::$documents[$documentID]->dataNodes as $dataNode) {
            if ($node->isSameNode($dataNode))
                return $dataNode;
        }
        // if doesn't, add it
        phpQuery::$documents[$documentID]->dataNodes[] = $node;
        return $node;
    }

    protected static function dataRemoveNode($node, $documentID)
    {
        // search are return if alredy exists
        foreach (phpQuery::$documents[$documentID]->dataNodes as $k => $dataNode) {
            if ($node->isSameNode($dataNode)) {
                unset(self::$documents[$documentID]->dataNodes[$k]);
                unset(self::$documents[$documentID]->data[$dataNode->dataID]);
            }
        }
    }

    public static function data($node, $name, $data, $documentID = null)
    {
        if (!$documentID)
            // TODO check if this works
            $documentID = self::getDocumentID($node);
        $document = phpQuery::$documents[$documentID];
        $node = self::dataSetupNode($node, $documentID);
        if (!isset($node->dataID))
            $node->dataID = ++phpQuery::$documents[$documentID]->uuid;
        $id = $node->dataID;
        if (!isset($document->data[$id]))
            $document->data[$id] = array();
        if (!is_null($data))
            $document->data[$id][$name] = $data;
        if ($name) {
            if (isset($document->data[$id][$name]))
                return $document->data[$id][$name];
        } else
            return $id;
    }

    public static function removeData($node, $name, $documentID)
    {
        if (!$documentID)
            // TODO check if this works
            $documentID = self::getDocumentID($node);
        $document = phpQuery::$documents[$documentID];
        $node = self::dataSetupNode($node, $documentID);
        $id = $node->dataID;
        if ($name) {
            if (isset($document->data[$id][$name]))
                unset($document->data[$id][$name]);
            $name = null;
            foreach ($document->data[$id] as $name)
                break;
            if (!$name)
                self::removeData($node, $name, $documentID);
        } else {
            self::dataRemoveNode($node, $documentID);
        }
    }
}

/**
 * Plugins static namespace class.
 *
 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 * @package phpQuery
 * @todo move plugin methods here (as statics)
 */
class phpQueryPlugins
{
    public function __call($method, $args)
    {
        if (isset(phpQuery::$extendStaticMethods[$method])) {
            $return = call_user_func_array(
                phpQuery::$extendStaticMethods[$method],
                $args
            );
        } else if (isset(phpQuery::$pluginsStaticMethods[$method])) {
            $class = phpQuery::$pluginsStaticMethods[$method];
            $realClass = "phpQueryPlugin_$class";
            $return = call_user_func_array(
                array($realClass, $method),
                $args
            );
            return isset($return)
                ? $return
                : $this;
        } else
            throw new Exception("Method '{$method}' doesnt exist");
    }
}

/**
 * Shortcut to phpQuery::pq($arg1, $context)
 * Chainable.
 *
 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 * @see phpQuery::pq()
 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 * @package phpQuery
 */
function pq($arg1, $context = null)
{
    $args = func_get_args();
    return call_user_func_array(
        array('phpQuery', 'pq'),
        $args
    );
}

// add plugins dir and Zend framework to include path
set_include_path(
    get_include_path()
    . PATH_SEPARATOR . dirname(__FILE__) . '/phpQuery/'
    . PATH_SEPARATOR . dirname(__FILE__) . '/phpQuery/plugins/'
);
// why ? no __call nor __get for statics in php...
// XXX __callStatic will be available in PHP 5.3
phpQuery::$plugins = new phpQueryPlugins();
// include bootstrap file (personal library config)
if (file_exists(dirname(__FILE__) . '/phpQuery/bootstrap.php'))
    require_once dirname(__FILE__) . '/phpQuery/bootstrap.php';
